Repository navigation
fix(forge,ai): clean machine-readable output — forge.error event, no litellm banner, one contract event - #858
Merged
Merged
Conversation
On a failed call litellm printed its "Give Feedback / Get Help" banner to stdout, ahead of `fluid ai test --json`'s report. quiet_litellm() sets litellm's own switch, suppress_debug_info (the one its Router and proxy set, BerriAI/litellm#5942), wherever forge loads litellm: _get_litellm, ai_setup._validate_api_key and the agent loop's tool call. The failure still reaches forge as the exception, and forge reports it as before. The tests use the installed litellm: mock_response=Exception() fails inside litellm's error mapping, where the banner is printed, with no network call.
… traceback A typed AI error such as copilot_no_llm_provider printed a full traceback on stderr: _create_project_minimal logged it with logger.exception, which the CLI's console handler prints at the default INFO level, in every mode. The --agent stream said only forge.done with exit_code 1. log_forge_failure (cli/_forge_mode_errors.py) logs a FLUID error once at ERROR without the traceback, kept off the console when the caller prints the message itself, and the traceback at DEBUG; a crash keeps logger.exception. It notes the failure in a ContextVar, and forge.run sends it as one forge.error event before forge.done: code, message, suggestions and docs_url, redacted; an unexpected exception by its class only, since MCP forge_run hands the events to the IDE's model. Template mode, the agent loop, the minimal path, the mode boundary and _run_main use it; _run_main now prints a FluidUserError's fix. A failed --seed-from notes the console's words, not its exception's text. MCP forge_run writes each forge.error to the server's log, since it discards forge's console output.
Blank mode emitted its own forge.contract_written and the receipt emitted another for the same file, first by its absolute path and then by its path from the current directory. Since #852 the receipt scans every --target-dir, so blank mode's own event goes, as in every other mode.
CHANGELOG: this branch's three Fixed entries move into the new, empty [Unreleased]; the 0.29.0 section is main's as cut.
…ng --verbose The console line ended "(run with --verbose for details)", and the new forge_seed_failed event repeated it as a suggestion, but forge has no --verbose flag (exit 2). Both now say what load_seed accepts: a *.odps.yaml or *.odcs.yaml file, or a directory of them.
_log_forge_errors wrote the forge.error message with %s, so a newline in it could add a made-up line to the server log. %r keeps it on one line (security re-check, LOW).
The help named forge.start, forge.done, forge.contract_written and forge.cost. forge.cost is never sent, and forge.error (new), forge.file_written and forge.plan were missing (delivery review, P2).
The summarizer imports litellm itself and calls litellm.completion, so it gets quiet_litellm like the other three callers (defect scan, Low). TestLogForgeFailure also drops the note it leaves, with the traceback frames it holds, after each test.
It named only *.odps.yaml / *.odcs.yaml files and directories; load_seed also takes .yml / .json names and any file with kind: DataProduct or DataContract (fix re-check, P3).
CHANGELOG [Unreleased] Fixed: keep both sides' entries, #856's two first, then this branch's three. fluid_build/cli/ai_setup.py merged cleanly.
The failure helpers catch Exception, not BaseException; the boundary test raises through a helper call inside the with, so the line after it is reachable; tests/test_litellm_banner.py imports fluid_build.cli.ai_setup one way.
This was referenced Nov 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR cleans up forge's machine-readable output by fixing three defects. All three are older than #850 to #854. There is no issue; the #854 session found the duplicate event.
1.
fluid forge --agentreports a failure as aforge.errorevent, not a traceback (fix(forge)).copilot_no_llm_providerprinted a full Python traceback on stderr._create_project_minimallogged it withlogger.exception, and the CLI's console handler prints that at the default INFO level, in every mode (--agentsets up no logging of its own). The stream said onlyforge.donewithexit_code1.log_forge_failure(cli/_forge_mode_errors.py) handles a FLUID error: theCLIErrorfamily,FluidError,FluidUserError, or a missing extra.fluid_console_shown, so it reaches--log-filebut not the console.logger.exception: its traceback is the bug report.ContextVar.forge.runsends it as oneforge.errorevent beforeforge.done.code,message,suggestionsand, when the error catalog has one,docs_url. This is the shape offluid ai test --json'serrorobject._console._redact_error_str, because the events go straight to stdout and skip the logging redactor.forge_runhands these events to the IDE's model (the copilot tools' typed-error rule, SECURITY_REVIEW I7).code: forge_unexpected_error,exception: <Class>).--seed-fromsends the console's own words, never the exception's text, which can carry a remote seed's body. Its next step now names what--seed-fromaccepts; it used to point at a--verboseflag that does not exist.forge_runthrows away forge's console output, so it now writes eachforge.errorto the server's log (%r, one line).ForgeModeErrorBoundary.__exit__and_run_main's last handler.CLIError's orFluidUserError's message and next steps, where it used to printForge failed: <event code>and a traceback._template_mode._is_missing_optionalis removed; the helper covers its case.--agenthelp now lists the events the stream really sends.forge.costwas listed but never sent;forge.error,forge.file_writtenandforge.planwere missing.--agent, stderr keeps the usual message and suggestion bullets (the founder's choice).forge.done, by design: a usage error (exit 2: the--prompt-profile,--prompt-overlayand--resumeflags), and a stop without an exception (blank mode finding a contract in place, a template not found, a rejected composition).2.
fluid ai test --jsonprints only its report when the call fails (fix(ai)).print(), ahead of the JSON.quiet_litellm()sets litellm's own switch,suppress_debug_info, at every forge call intolitellm.completion:_get_litellmai_setup._validate_api_keyprintsites in litellm 1.104.0 and 1.109.1: the banner, and a "Provider List" link whose URL is also in the exception text.fluid ai testoutput on a failed call differs from main only by the 4 banner lines.3.
fluid forge --agent --blankreports its contract once (fix(forge)).forge.contract_written, and the receipt emitted another for the same file: first by its absolute path, then by its path from the current directory.--target-dir, so blank mode's own event goes, as in every other mode.Design note.
fluid forge --agentand parses stdout line by line.forge_runhands the events to the IDE's model.forge.start, then oneforge.contract_writtenper file, thenforge.donewithexit_code0.forge.error {code, message, suggestions, docs_url}, thenforge.donewith a non-zeroexit_code.ai test --json.--debugorFLUID_LOG_LEVEL=DEBUG; a crash keeps its traceback.forge.<noun>vocabulary.Overlap: the "Provider selection" branch also touches
fluid_build/cli/forge.pyandfluid_build/cli/ai_setup*.py.forge.py:run(and two new helpers below it),_run_main's lastexcept,_run_blank_mode, and the--agenthelp string;ai_setup.py: two lines in_validate_api_key.Type of Change
Documentation
Pages to update in forge_docs:
docs/cli/forge.md(Headless agent mode):forge.error: its fields, its place beforeforge.done, and which failures send none;forge.contract_written, reported by its path from the current directory, or by its absolute path outside it.docs/cli/ai.md: on a failed call,fluid ai test --jsonstdout is the report alone, with no litellm banner.docs/cli/mcp.md:forge_runresults can carry aforge.errorevent, and a failure is also logged on the server.advanced/production-troubleshooting:--debugorFLUID_LOG_LEVEL=DEBUGshows a FLUID error's traceback.upgrading: a short "on main" note for the three behaviour changes.Checklist
pytest)ruff checkandblackwith no errorspython scripts/add_license_headers.py;examples/**exempt)Testing
Borrow receipts.
ai test --json's error object;fluid_console_shownconsole filter (cli/__init__.py,build_runners/_lineage.py);_redact_error_strand the_error_catalogdocs URLs;suppress_debug_infogates and its Router.suppress_debug_info: dspy'sdisable_litellm_loggingsets the same switch;exec --json: anerrorevent plus aturn.failedterminal event;stream-json:errorevents plus a finalresultwith a status.Tests that fail on main and pass here. 30 new or extended tests, checked on a detached
origin/mainworktree with only the test files copied in.tests/cli/test_forge_agent_mode.py, sections 8 and 9, and the--agenthelp test:--seed-from;forge_runlogging;tests/cli/test_forge_mode_errors.py: the helper,describe_forge_failure, the boundary, the agent loop, template mode, and_run_mainprinting a fix.tests/test_litellm_banner.py: uses the installed litellm, made to fail withmock_response=Exception(...)and no network. It covers_get_litellm,ai test --json,ai setup's key check and the agent loop. It skips when a litellm prints no banner.Gates, on the final tree.
ruff check fluid_build/ tests/--self-testscripts/check_license_headers.pylint-imports(6 kept)scripts/mypy_strict.pydetect-secrets-hook1.5.0 over the changed files, fed through xargs, returns 0. The only baseline entry in a touched file (ai_setup.py:154) sits above the edits.ripwire --quality-deltaon the diff applied to a pristine main worktree exits 2. Pristine main exits 2 on its own (23 dead-code rows with no change at all). This diff adds 3 short-horizon-churn rows and 2 small test-shape clones, and no complexity, nesting or params regression.Tests in a lean
.[dev,local]venv.tests/perf/test_startup_budget.pyandtests/cli/test_lazy_core_commands.py.tests/providers/test_duckdb_sandbox_workspace_root.pytests fail the same way on main.test_dotenv_pairing_matrix::test_an_operator_exported_aws_endpoint_is_usedandtest_iac_databricks_metadata_fake_e2e::test_certified_then_deprecated....Live runs through the
fluidCLI, on the final tree.forge --agent --context x:forge.start,forge.error(copilot_no_llm_provider),forge.done. No traceback at INFO; one at DEBUG.--seed-fromon a missing file:forge_seed_failed.--llm-provider anthropicwithout a key:copilot_missing_llm_api_key.--blank: oneforge.contract_written.--template analytics: oneforge.contract_written.ai test --jsonwith a dummy key: one JSON document, witherror.codecopilot_litellm_request_failed.Reviews (/ship gate): passed after 1 fix round.
--agenthelp missingforge.error. It is fixed, and a re-check confirmed all five fixes. UX lane: PASS.forge.error") is NOT_A_BUG: the tworeturn 1sites are untouched, emit what they did on main, and the behaviour is documented and pinned by a test. LOWs fixed here: the summarizer now quiets litellm, and the new tests clean up their note.Follow-ups, not in this PR:
forge.errorfor stops without an exception: template not found, a rejected composition, an existing blank contract.forge.errorfor usage errors.mcp_sampling_failedand litellm messages embed (redacted) exception text that now reaches the IDE's model.gsk_,?key=, SASsig=, a bare AWS secret. Keyring keys are neverregister_secreted._run_forge_inproctreats any captured JSON line with aneventkey as a real event.forge_runcalls share the global stdout redirect.Screenshots / Output
fluid forge --agent --context x -d prodwith no provider. Before (origin/main):After:
fluid ai test --jsonon a failed call. Before: four banner lines (Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new...) came ahead of{. After: the JSON report alone.